package shell;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import minmax.GameMove;
import minmax.GamePair;
import minmax.GameSentinel;
import minmax.GameSentinel.ErrorCase;
import minmax.GameSentinel.SentinelException;
import minmax.GameState;
import minmax.MinMax;

/**
 * general game implementation
 * 
 * @author alex
 *
 * @param <M> is type of moves
 * @param <S> is type of state
 */
public abstract class AbstractGame<M extends GameMove, S extends GameState<M>> {
	
	/**
	 * debug mode
	 */
	private boolean isDebugging;
	
	/**
	 * verbose debug output
	 */
	private boolean verbose;
	
	/**
	 * verifying debug mode
	 */
	private boolean verify;
	
	/**
	 * resets history after each successful move
	 */
	private boolean clean;
	
	/**
	 * Counts how many games have been played
	 */
	private int gameCounter = 0;
	
	/**
	 * Counts how many times player one has won
	 */
	private int playerOneWins = 0;
	
	/**
	 * Counts how many times player two has won
	 */
	private int playerTwoWins = 0;
	
	/**
	 * Limiter on how many games should be played before finishing.
	 */
	private int gamesToPlay = 50;
	
	/**
	 * min-max engine
	 */
	private MinMax engine;
	
	/**
	 * state filter
	 */
	private Class<S> stateClass;

	/**
	 * original state
	 */
	private S unfoldedState;
	
	/**
	 * state
	 */
	private GameState<M> state;
	
	/**
	 * verifier
	 */
	private GameSentinel<M> sentinel;
	
	/**
	 * first player handler
	 */
	private Player firstPlayer;
	
	/**
	 * second player handler
	 */
	private Player secondPlayer;
	
	/**
	 * input stream
	 */
	private final LineNumberReader reader;
	
	/**
	 * Boolean to keep track of whether to show a GUI or not.
	 */
	private boolean showGui = false;
	
	/**
	 * set of predefined player names
	 */
	private static final Set<String> PREDEFINED_PLAYERS;
	
	/**
	 * consists of just a "computer"
	 */
	private static final Set<String> DEFAULT_STATES;
	
	static {
		PREDEFINED_PLAYERS = new HashSet<String>(2);
		PREDEFINED_PLAYERS.add("human");
		PREDEFINED_PLAYERS.add("random");
		DEFAULT_STATES = Collections.singleton("computer");
	}
	
	/**
	 * prints line of text
	 * 
	 * @param line is a text
	 */
	private void writeln(String line) {
		System.out.println(line);
	}
	
	/**
	 * reads a line of text
	 * 
	 * @return text
	 * @throws IOException
	 */
	private String readln() throws IOException {
		return reader.readLine().trim().toLowerCase();
	}
	
	/**
	 * create the game
	 * 
	 * @param stateClass is class of the state implementation
	 * @param depth is the default depth
	 * @param args is the arguments
	 */
	public AbstractGame(Class<S> stateClass, int depth, String[] args) {
		if (stateClass == null) {
			throw new NullPointerException();
		}
		if (depth < 1) {
			throw new IllegalArgumentException("default depth can't be < 1");
		}
		this.stateClass = stateClass;
		int minMaxDepth = depth;
		this.reader = new LineNumberReader(new InputStreamReader(System.in));
		String first = "human", second = "computer";
		// process arguments
		if (args.length > 0) {
			Set<String> params = new HashSet<String>(args.length);
			for (String arg : args) {
				String param = arg.toLowerCase();
				if (param.startsWith("-depth=")) {
					String warning = null;
					try {
						int parsed = Integer.parseInt(param.substring("-depth=".length()));
						if (parsed < 1) {
							warning = "Depth must be >= 1, using default one : " + depth;
						} else {
							minMaxDepth = parsed;
						}
					} catch(NumberFormatException e) {
						warning = "Unparsable depth, using default one : " + depth;
					}
					if (warning != null) {
						writeln(warning);
					}
				} if (param.startsWith("-first=")) {
					first = param.substring("-first=".length());
				} if (param.startsWith("-second=")) {
					second = param.substring("-second=".length());
				} if (param.startsWith("-gui")) {
					showGui = true;
				} else {
					params.add(param);
				}
			}
			// initialize debug mode
			if (params.contains("-debug")) {
				isDebugging = true;
				verbose = params.contains("-verbose");
				verify = params.contains("-verify");
				clean = params.contains("-clean");
			}
		}
		// create min-max engine
		engine = createEngine(minMaxDepth);
		// start the game
		reset(first, second);
	}
	
	/**
	 * creates min-max engine with a given depth
	 * 
	 * @param depth is min-max algorithm bound
	 * @return engine
	 */
	protected MinMax createEngine(int depth) {
		return new MinMax(depth);
	}
	
	/**
	 * description of the game and playable implementations
	 * 
	 * @return text
	 */
	protected String description() {
		return getClass().getSimpleName();
	}
	
	/**
	 * gives names of the available state implementations,
	 * including "computer".
	 * 
	 * @return unmodifiable non-empty list
	 */
	protected Set<String> availableStates() {
		return DEFAULT_STATES;
	}

	/**
	 * creates a named state, unknown name result in the default implementation
	 * 
	 * @param name is a state name, or "computer" which stands for the default one
	 * @return created state
	 */
	protected abstract S createState(String name);
	
	/**
	 * converts state into printable form
	 * 
	 * @param state is a game state
	 * @return string
	 */
	protected abstract String render(S state);
	
	/**
	 * Displays GUI.
	 * 
	 * @param state is a game state
	 * 
	 */
	protected abstract void displayGUI(S state);
	
	/**
	 * parses a game-specific command into move
	 * 
	 * @param state is a game state
	 * @param command is a text
	 * @return is a move or <code>null</code> when command is incorrect
	 */
	protected abstract M parseMove(S state, String command);
	
	/**
	 * Waits for the user to do a move through the GUI
	 * @param state is a game state
	 * @return is a move or <code>null</code> when command is incorrect
	 */
	protected abstract M waitForMove(S state);
	
	/**
	 * resets a game
	 * 
	 * @param first is name of the first player engine
	 * @param second is name of the second player engine
	 */
	protected void reset(String first, String second) {
		// determine which providers are in use
		Set<String> providers = new LinkedHashSet<String>(2);
		providers.add(first);
		providers.add(second);
		providers.removeAll(PREDEFINED_PLAYERS);
		// create providers
		S firstState, secondState;
		switch (providers.size()) {
		case 0:
			firstState = createState("computer");
			secondState = firstState;
			break;
		case 1:
			firstState = createState(providers.iterator().next());
			secondState = firstState;
			break;
		case 2:
			Iterator<String> i = providers.iterator();
			firstState = createState(i.next());
			secondState = createState(i.next());
			break;
		default:
			throw new AssertionError("unexpected set size : " + providers.size());
		}
		// create players
		String[] players = {first, second};
		boolean isFirst = true;
		for (String player : players) {
			Player instance;
			if ("human".equals(player)) {
				instance = new HumanPlayer(this);
			} else if ("random".equals(player)) {
				instance = new RandomPlayer(this);
			} else {
				instance = new ComputerPlayer(this, isFirst ? first : second);
			}
			if (isFirst) {
				firstPlayer = instance;
			} else {
				secondPlayer = instance;
			}
			isFirst = !isFirst;
		}
		// assign the state and debugger
		unfoldedState = firstState;
		if (providers.size() == 2) {
			state = new GamePair<M>(firstState, secondState);
		} else {
			state = unfoldedState;
		}
		if (isDebugging) {
			sentinel = new GameSentinel<M>(state, verbose, verify);
			state = sentinel;
		}
	}
	
	/**
	 * unfolds the underlying game implementation
	 * 
	 * @param state is a game state, possibly wrapped
	 * @return unfolded state
	 */
	private S unfold(GameState<M> state) {
		if (state instanceof GamePair) {
			state = ((GamePair<M>)state).unfold();
		}
		return stateClass.cast(state);
	}
	
	/**
	 * game loop
	 * 
	 * @throws IOException if failed to read from console
	 */
	private void game() throws IOException {
		// game introduction
		writeln(description());
		writeln("General commands are : quit - terminates the program, reset [player1 player2] - to start the new game [with given players],");
		writeln("where each player can be one of the following : ");
		writeln("-human");
		writeln("-random");
		for (String name : availableStates()) {
			writeln("-" + name);
		}
		if (isDebugging) {
			writeln("Type fail to mark current state as invalid.");
		}
		GameLoop: while (true) {
			// display initial game
			writeln("");
			writeln("New game started: Player 1 is " + firstPlayer.name() + ", Player 2 is " + secondPlayer.name());
			if(showGui)
				displayGUI(unfoldedState);
			
			writeln(render(unfoldedState));
			boolean player = false;
			while (!state.isFinished()) {
				Player currentPlayer = player ? secondPlayer : firstPlayer;
				writeln("Player " + (player ? "2" : "1") + " [" + currentPlayer.name() + "] moves:");
				String[] reply = currentPlayer.move(state);
				if (reply != null) {
					if (reply.length == 0) {
						break GameLoop;
					} else {
						reset(reply[0], reply[1]);
						continue GameLoop;
					}
				}
				if (isDebugging && clean) {
					sentinel.clear();
				}
				// display game state
				if(showGui)
					displayGUI(unfoldedState);
				
				writeln(render(unfoldedState));
				player = !player;
			}
			// display game result
			switch (state.outcome()) {
			case Draw:
				writeln("Draw");
				break;
			case First:
				writeln("First player [" + firstPlayer.name() + "] wins");
				playerOneWins++;
				break;
			case Second:
				writeln("Second player [" + secondPlayer.name() + "] wins");
				playerTwoWins++;
				break;
			default:
				throw new AssertionError("unknown constant");
			}
			
			// TODO add reset & quit here too
			//readln();
			
			gameCounter++;
			
			// immediately start new game session
			System.out.println("Games played: " + gameCounter);
			System.out.println(firstPlayer.name() + " " + playerOneWins + " - " + playerTwoWins + " " + secondPlayer.name());
			if(gameCounter<gamesToPlay)
				reset(firstPlayer.name(), secondPlayer.name());
			else
				break;
		}
	}
	
	/**
	 * allows to navigate through a history of moves
	 * 
	 * @param description is an error case
	 */
	private void viewer(ErrorCase<M> description) throws IOException {
		writeln("");
		writeln("[Debug mode initiated] : user empty command for forward, back for backward, restart to reset the viewer and quit to exit.");
		writeln("Error has occured : " + description.type);
		if (description.cause != null) {
			writeln("Failure is caused by exception : " + description.cause);
		}
		if (description.movesIndex >= 0) {
			writeln("Moves index : " + description.movesIndex);
		}
		if (description.historyIndex >= 0) {
			writeln("History index : " + description.historyIndex);
		}
		ViewerLoop: while (true) {
			GameState<M> state = description.initialState.clone();
			List<M> moves = description.history == null ? description.moves : description.history;
			int step = 0;
			try {
				while (true) {
					writeln("Step " + step);
					writeln(render(unfold(state)));
					writeln("_/back/reset/quit");
					String command = readln();
					if (command.length() == 0) {
						if (step < moves.size()) {
							state.move(moves.get(step));
							step++;
						}
					} else if ("back".equals(command)) {
						if (step > 0) {
							step--;
							state.backtrack();
						}
					} else if ("reset".equals(command)) {
						break;
					} else if ("quit".equals(command)) {
						break ViewerLoop;
					} else {
						writeln("Unknown command.");
					}
				}
			} catch (IOException e) {
				throw e;
			} catch (Exception e) {
				writeln("Exception occured : " + e);
				writeln("Press enter to continue");
				readln();
			}
		}
	}
	
	/**
	 * runs the game
	 */
	public final void play() {
		try {
			try {
				game();
			} catch(SentinelException e) {
				viewer(sentinel.description());
			}
		} catch (IOException e) {
			writeln("i/o error occured: " + e.getMessage());
		}
	}
	
	
	/**
	 * abstract player handler
	 */
	private abstract class Player {
		
		private String name;
		
		public Player(String name) {
			this.name = name;
		}
		
		public String name() {
			return name;
		}
		
		public abstract String[] move(GameState<M> state) throws IOException;
		
	}
	
	/**
	 * reads next move from console
	 */
	private class HumanPlayer extends Player {

		public HumanPlayer(AbstractGame<M, S> game) {
			game.super("human");
		}

		@Override
		public String[] move(GameState<M> state) throws IOException {
			// read a move
			M step = null;
			while (step == null) {
				if(showGui) {
					// Wait for GUI interaction
					step = waitForMove(unfoldedState);
				}
				else {
					String command = readln();
					if (command.startsWith("quit")) {
						return new String[0];
					} else if (command.startsWith("reset")) {
						String[] parts = command.split("\\s");
						if (parts.length == 3) {
							return new String[] {parts[1], parts[2]};
						}
					} else if (isDebugging && command.startsWith("fail")) {
						sentinel.fail();
					} else {
						step = parseMove(unfoldedState, command);
						if (step != null && !state.canMove(step)) {
							step = null;
						}
					}
				}
				// report error
				if (step == null) {
					writeln("Move/Command not recognized");
				}
			}
			// make the move
			state.move(step);
			return null;
		}
		
	}
	
	/**
	 * uses min-max to determine next move
	 */
	private class ComputerPlayer extends Player {
		
		public ComputerPlayer(AbstractGame<M, S> game, String name) {
			game.super(name);
		}

		@Override
		public String[] move(GameState<M> state) {
			M step = engine.minmax(state);
			state.move(step);
			return null;
		}
		
	}
	
	/**
	 * chooses a valid move at random
	 */
	private class RandomPlayer extends Player {
		
		private Random random;
		
		private List<M> moves;

		public RandomPlayer(AbstractGame<M, S> game) {
			game.super("random");
			random = new Random();
			moves = new ArrayList<M>();
		}

		@Override
		public String[] move(GameState<M> state) {
			moves.clear();
			Iterator<M> i = state.moves();
			while (i.hasNext()) {
				moves.add(i.next());
			}
			try {
				M step = moves.get(random.nextInt(moves.size()));
				state.move(step);
			}
			catch (IllegalArgumentException e){
				System.out.println("Number of moves: " + moves.size());
			}
			
			return null;
		}
		
	}
	
}
